// Copyright (C) 2017 ScyllaDB
// Use of this source code is governed by a ALv2-style
// license that can be found in the LICENSE file.

//go:build all || integration
// +build all integration

package gocqlx_test

import (
	"math/big"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/gocql/gocql"
	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"gopkg.in/inf.v0"

	"github.com/scylladb/gocqlx/v3"
	"github.com/scylladb/gocqlx/v3/gocqlxtest"
	"github.com/scylladb/gocqlx/v3/qb"
)

type FullName struct {
	FirstName string
	LastName  string
}

func (n FullName) MarshalCQL(info gocql.TypeInfo) ([]byte, error) {
	return []byte(n.FirstName + " " + n.LastName), nil
}

func (n *FullName) UnmarshalCQL(info gocql.TypeInfo, data []byte) error {
	t := strings.SplitN(string(data), " ", 2)
	n.FirstName, n.LastName = t[0], t[1]
	return nil
}

type FullNameUDT struct {
	gocqlx.UDT
	FullName
}

type FullNamePtrUDT struct {
	gocqlx.UDT
	*FullName
}

func diff(t *testing.T, expected, got interface{}) {
	t.Helper()

	if d := cmp.Diff(expected, got, diffOpts); d != "" {
		t.Errorf("got %+v expected %+v, diff: %s", got, expected, d)
	}
}

var diffOpts = cmpopts.IgnoreUnexported(big.Int{}, inf.Dec{})

func TestIterxUDT(t *testing.T) {
	session := gocqlxtest.CreateSession(t)
	t.Cleanup(func() {
		session.Close()
	})

	if err := session.ExecStmt(`CREATE TYPE gocqlx_test.UDTTest_Full (first text, second text)`); err != nil {
		t.Fatal("create type:", err)
	}

	if err := session.ExecStmt(`CREATE TABLE gocqlx_test.udt_table (
			testuuid       timeuuid PRIMARY KEY,
			testudt        gocqlx_test.UDTTest_Full
		)`); err != nil {
		t.Fatal("create table:", err)
	}

	type Full struct {
		First  string
		Second string
	}

	type Part struct {
		First string
	}

	type Extra struct {
		First  string
		Second string
		Third  string
	}

	type FullUDT struct {
		gocqlx.UDT
		Full
	}

	type PartUDT struct {
		gocqlx.UDT
		Part
	}

	type ExtraUDT struct {
		gocqlx.UDT
		Extra
	}

	type FullUDTPtr struct {
		gocqlx.UDT
		*Full
	}

	type PartUDTPtr struct {
		gocqlx.UDT
		*Part
	}

	type ExtraUDTPtr struct {
		gocqlx.UDT
		*Extra
	}

	full := FullUDT{
		Full: Full{
			First:  "John",
			Second: "Doe",
		},
	}

	makeStruct := func(testuuid gocql.UUID, insert interface{}) interface{} {
		b := reflect.New(reflect.StructOf([]reflect.StructField{
			{
				Name: "TestUUID",
				Type: reflect.TypeOf(gocql.UUID{}),
			},
			{
				Name: "TestUDT",
				Type: reflect.TypeOf(insert),
			},
		})).Interface()
		reflect.ValueOf(b).Elem().FieldByName("TestUUID").Set(reflect.ValueOf(testuuid))
		reflect.ValueOf(b).Elem().FieldByName("TestUDT").Set(reflect.ValueOf(insert))
		return b
	}

	tcases := []struct {
		name         string
		insert       interface{}
		expected     interface{}
		expectedOnDB FullUDT
	}{
		{
			name:         "exact-match",
			insert:       full,
			expectedOnDB: full,
			expected:     full,
		},
		{
			name: "exact-match-ptr",
			insert: FullUDTPtr{
				Full: &Full{
					First:  "John",
					Second: "Doe",
				},
			},
			expectedOnDB: full,
			expected: FullUDTPtr{
				Full: &Full{
					First:  "John",
					Second: "Doe",
				},
			},
		},
		{
			name: "extra-field",
			insert: ExtraUDT{
				Extra: Extra{
					First:  "John",
					Second: "Doe",
					Third:  "Smith",
				},
			},
			expectedOnDB: full,
			expected: ExtraUDT{
				Extra: Extra{
					First:  "John",
					Second: "Doe",
					Third:  "", // Since the UDT has only 2 fields, the third field should be empty
				},
			},
		},
		{
			name: "extra-field-ptr",
			insert: ExtraUDTPtr{
				Extra: &Extra{
					First:  "John",
					Second: "Doe",
					Third:  "Smith",
				},
			},
			expectedOnDB: full,
			expected: ExtraUDTPtr{
				Extra: &Extra{
					First:  "John",
					Second: "Doe",
					Third:  "", // Since the UDT has only 2 fields, the third field should be empty
				},
			},
		},
		{
			name: "absent-field",
			insert: PartUDT{
				Part: Part{
					First: "John",
				},
			},
			expectedOnDB: FullUDT{
				Full: Full{
					First:  "John",
					Second: "",
				},
			},
			expected: PartUDT{
				Part: Part{
					First: "John",
				},
			},
		},
		{
			name: "absent-field-ptr",
			insert: PartUDTPtr{
				Part: &Part{
					First: "John",
				},
			},
			expectedOnDB: FullUDT{
				Full: Full{
					First:  "John",
					Second: "",
				},
			},
			expected: PartUDTPtr{
				Part: &Part{
					First: "John",
				},
			},
		},
	}

	const insertStmt = `INSERT INTO udt_table (testuuid, testudt) VALUES (?, ?)`
	const deleteStmt = `DELETE FROM udt_table WHERE testuuid = ?`

	for _, tc := range tcases {
		t.Run(tc.name, func(t *testing.T) {
			testuuid := gocql.TimeUUID()

			if reflect.TypeOf(tc.insert) != reflect.TypeOf(tc.expected) {
				t.Fatalf("insert and expectedOnDB must have the same type")
			}

			t.Cleanup(func() {
				_ = session.Query(deleteStmt, nil).Bind(testuuid).ExecRelease()
			})

			t.Run("insert-bind", func(t *testing.T) {
				if err := session.Query(insertStmt, nil).Bind(
					testuuid,
					tc.insert,
				).ExecRelease(); err != nil {
					t.Fatal(err.Error())
				}

				// Make sure the UDT was inserted correctly
				v := FullUDT{}
				if err := session.Query(`SELECT testudt FROM udt_table where testuuid = ?`, nil).Bind(testuuid).Get(&v); err != nil {
					t.Fatal(err.Error())
				}
				diff(t, tc.expectedOnDB, v)
			})

			t.Run("scan", func(t *testing.T) {
				v := reflect.New(reflect.TypeOf(tc.expected)).Interface()
				if err := session.Query(`SELECT testudt FROM udt_table where testuuid = ?`, nil).Bind(testuuid).Scan(v); err != nil {
					t.Fatal(err.Error())
				}
				diff(t, tc.expected, reflect.ValueOf(v).Elem().Interface())
			})

			t.Run("get", func(t *testing.T) {
				v := reflect.New(reflect.TypeOf(tc.expected)).Interface()
				if err := session.Query(`SELECT testudt FROM udt_table where testuuid = ?`, nil).Bind(testuuid).Get(v); err != nil {
					t.Fatal(err.Error())
				}
				diff(t, tc.expected, reflect.ValueOf(v).Elem().Interface())
			})

			t.Run("delete", func(t *testing.T) {
				if err := session.Query(deleteStmt, nil).Bind(
					testuuid,
				).ExecRelease(); err != nil {
					t.Fatal(err.Error())
				}
			})

			t.Run("insert-bind-struct", func(t *testing.T) {
				b := makeStruct(testuuid, tc.insert)
				if err := session.Query(insertStmt, []string{"test_uuid", "test_udt"}).BindStruct(b).ExecRelease(); err != nil {
					t.Fatal(err.Error())
				}

				// Make sure the UDT was inserted correctly
				v := reflect.New(reflect.TypeOf(tc.expectedOnDB)).Interface()
				if err := session.Query(`SELECT testudt FROM udt_table where testuuid = ?`, nil).Bind(testuuid).Get(v); err != nil {
					t.Fatal(err.Error())
				}
				diff(t, &tc.expectedOnDB, v)
			})

			t.Run("insert-bind-struct-map", func(t *testing.T) {
				t.Run("empty-map", func(t *testing.T) {
					b := makeStruct(testuuid, tc.insert)
					if err := session.Query(insertStmt, []string{"test_uuid", "test_udt"}).
						BindStructMap(b, nil).ExecRelease(); err != nil {
						t.Fatal(err.Error())
					}

					// Make sure the UDT was inserted correctly
					v := reflect.New(reflect.TypeOf(tc.expectedOnDB)).Interface()
					if err := session.Query(`SELECT testudt FROM udt_table where testuuid = ?`, nil).Bind(testuuid).Get(v); err != nil {
						t.Fatal(err.Error())
					}
					diff(t, &tc.expectedOnDB, v)
				})

				t.Run("empty-struct", func(t *testing.T) {
					if err := session.Query(insertStmt, []string{"test_uuid", "test_udt"}).
						BindStructMap(struct{}{}, map[string]interface{}{
							"test_uuid": testuuid,
							"test_udt":  tc.insert,
						}).ExecRelease(); err != nil {
						t.Fatal(err.Error())
					}

					// Make sure the UDT was inserted correctly
					v := reflect.New(reflect.TypeOf(tc.expectedOnDB)).Interface()
					if err := session.Query(`SELECT testudt FROM udt_table where testuuid = ?`, nil).Bind(testuuid).Get(v); err != nil {
						t.Fatal(err.Error())
					}
					diff(t, &tc.expectedOnDB, v)
				})
			})

			t.Run("insert-bind-map", func(t *testing.T) {
				if err := session.Query(insertStmt, []string{"test_uuid", "test_udt"}).
					BindMap(map[string]interface{}{
						"test_uuid": testuuid,
						"test_udt":  tc.insert,
					}).ExecRelease(); err != nil {
					t.Fatal(err.Error())
				}

				// Make sure the UDT was inserted correctly
				v := reflect.New(reflect.TypeOf(tc.expectedOnDB)).Interface()
				if err := session.Query(`SELECT testudt FROM udt_table where testuuid = ?`, nil).Bind(testuuid).Get(v); err != nil {
					t.Fatal(err.Error())
				}
				diff(t, &tc.expectedOnDB, v)
			})
		})
	}
}

func TestIterxStruct(t *testing.T) {
	session := gocqlxtest.CreateSession(t)
	defer session.Close()

	if err := session.ExecStmt(`CREATE TYPE gocqlx_test.FullName (first_Name text, last_name text)`); err != nil {
		t.Fatal("create type:", err)
	}

	if err := session.ExecStmt(`CREATE TABLE gocqlx_test.struct_table (
			testuuid       timeuuid PRIMARY KEY,
			testtimestamp  timestamp,
			testvarchar    varchar,
			testbigint     bigint,
			testblob       blob,
			testbool       boolean,
			testfloat      float,
			testdouble     double,
			testint        int,
			testdecimal    decimal,
			testlist       list<text>,
			testset        set<int>,
			testmap        map<varchar, varchar>,
			testvarint     varint,
			testinet       inet,
			testcustom     text,
			testudt        gocqlx_test.FullName,
			testptrudt     gocqlx_test.FullName
		)`); err != nil {
		t.Fatal("create table:", err)
	}

	type StructTable struct {
		Testuuid      gocql.UUID
		Testvarchar   string
		Testbigint    int64
		Testtimestamp time.Time
		Testblob      []byte
		Testbool      bool
		Testfloat     float32
		Testdouble    float64
		Testint       int
		Testdecimal   *inf.Dec
		Testlist      []string
		Testset       []int
		Testmap       map[string]string
		Testvarint    *big.Int
		Testinet      string
		Testcustom    FullName
		Testudt       FullNameUDT
		Testptrudt    FullNamePtrUDT
	}

	bigInt := new(big.Int)
	if _, ok := bigInt.SetString("830169365738487321165427203929228", 10); !ok {
		t.Fatal("failed setting bigint by string")
	}

	m := StructTable{
		Testuuid:      gocql.TimeUUID(),
		Testvarchar:   "Test VarChar",
		Testbigint:    time.Now().Unix(),
		Testtimestamp: time.Now().Truncate(time.Millisecond).UTC(),
		Testblob:      []byte("test blob"),
		Testbool:      true,
		Testfloat:     float32(4.564),
		Testdouble:    float64(4.815162342),
		Testint:       2343,
		Testdecimal:   inf.NewDec(100, 0),
		Testlist:      []string{"quux", "foo", "bar", "baz", "quux"},
		Testset:       []int{1, 2, 3, 4, 5, 6, 7, 8, 9},
		Testmap:       map[string]string{"field1": "val1", "field2": "val2", "field3": "val3"},
		Testvarint:    bigInt,
		Testinet:      "213.212.2.19",
		Testcustom:    FullName{FirstName: "John", LastName: "Doe"},
		Testudt:       FullNameUDT{FullName: FullName{FirstName: "John", LastName: "Doe"}},
		Testptrudt:    FullNamePtrUDT{FullName: &FullName{FirstName: "John", LastName: "Doe"}},
	}

	const insertStmt = `INSERT INTO struct_table (
                          testuuid, testtimestamp, testvarchar, testbigint, testblob, testbool, testfloat, testdouble, 
                          testint, testdecimal, testlist, testset, testmap, testvarint, testinet, testcustom, testudt, testptrudt
                          ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`

	if err := session.Query(insertStmt, nil).Bind(
		m.Testuuid,
		m.Testtimestamp,
		m.Testvarchar,
		m.Testbigint,
		m.Testblob,
		m.Testbool,
		m.Testfloat,
		m.Testdouble,
		m.Testint,
		m.Testdecimal,
		m.Testlist,
		m.Testset,
		m.Testmap,
		m.Testvarint,
		m.Testinet,
		m.Testcustom,
		m.Testudt,
		m.Testptrudt).ExecRelease(); err != nil {
		t.Fatal("insert:", err)
	}

	const stmt = `SELECT * FROM struct_table`

	t.Run("get", func(t *testing.T) {
		var v StructTable
		if err := session.Query(stmt, nil).Get(&v); err != nil {
			t.Fatal("Get() failed:", err)
		}
		if diff := cmp.Diff(m, v, diffOpts); diff != "" {
			t.Fatalf("Get()=%+v expected %+v, diff: %s", v, m, diff)
		}
	})

	t.Run("select", func(t *testing.T) {
		var v []StructTable
		if err := session.Query(stmt, nil).Select(&v); err != nil {
			t.Fatal("Select() failed:", err)
		}
		if len(v) != 1 {
			t.Fatalf("Select()=%+v expected 1 row got %d", v, len(v))
		}
		if diff := cmp.Diff(m, v[0], diffOpts); diff != "" {
			t.Fatalf("Select()[0]=%+v expected %+v, diff: %s", v[0], m, diff)
		}
	})

	t.Run("select ptr", func(t *testing.T) {
		var v []*StructTable
		if err := session.Query(stmt, nil).Select(&v); err != nil {
			t.Fatal("Select() failed:", err)
		}
		if len(v) != 1 {
			t.Fatalf("Select()=%+v expected 1 row got %d", v, len(v))
		}
		if diff := cmp.Diff(&m, v[0], diffOpts); diff != "" {
			t.Fatalf("Select()[0]=%+v expected %+v, diff: %s", v[0], &m, diff)
		}
	})

	t.Run("struct scan", func(t *testing.T) {
		var (
			v StructTable
			n int
		)

		iter := session.Query(stmt, nil).Iter()
		for iter.StructScan(&v) {
			n++
		}
		if err := iter.Close(); err != nil {
			t.Fatal("StructScan() failed:", err)
		}
		if n != 1 {
			t.Fatalf("StructScan() expected 1 row got %d", n)
		}
		if diff := cmp.Diff(m, v, diffOpts); diff != "" {
			t.Fatalf("StructScan()=%+v expected %+v, diff: %s", v, m, diff)
		}
	})
}

func TestIterxScannable(t *testing.T) {
	session := gocqlxtest.CreateSession(t)
	defer session.Close()

	if err := session.ExecStmt(`CREATE TABLE gocqlx_test.scannable_table (testfullname text PRIMARY KEY)`); err != nil {
		t.Fatal("create table:", err)
	}

	m := FullName{"John", "Doe"}

	if err := session.Query(`INSERT INTO scannable_table (testfullname) values (?)`, nil).Bind(m).Exec(); err != nil {
		t.Fatal("insert:", err)
	}

	const stmt = `SELECT testfullname FROM scannable_table`

	t.Run("get", func(t *testing.T) {
		var v FullName
		if err := session.Query(stmt, nil).Get(&v); err != nil {
			t.Fatal("Get() failed:", err)
		}
		if diff := cmp.Diff(m, v); diff != "" {
			t.Fatalf("Get()=%+v expected %+v, diff: %s", v, m, diff)
		}
	})

	t.Run("select", func(t *testing.T) {
		var v []FullName
		if err := session.Query(stmt, nil).Select(&v); err != nil {
			t.Fatal("Select() failed:", err)
		}
		if len(v) != 1 {
			t.Fatalf("Select()=%+v expected 1 row got %d", v, len(v))
		}
		if diff := cmp.Diff(m, v[0]); diff != "" {
			t.Fatalf("Select()[0]=%+v expected %+v, diff: %s", v[0], m, diff)
		}
	})

	t.Run("select ptr", func(t *testing.T) {
		var v []*FullName
		if err := session.Query(stmt, nil).Select(&v); err != nil {
			t.Fatal("Select() failed:", err)
		}
		if len(v) != 1 {
			t.Fatalf("Select()=%+v expected 1 row got %d", v, len(v))
		}
		if diff := cmp.Diff(&m, v[0]); diff != "" {
			t.Fatalf("Select()[0]=%+v expected %+v, diff: %s", v[0], &m, diff)
		}
	})
}

func TestIterxStructOnly(t *testing.T) {
	session := gocqlxtest.CreateSession(t)
	defer session.Close()

	if err := session.ExecStmt(`CREATE TABLE gocqlx_test.struct_only_table (first_name text, last_name text, PRIMARY KEY (first_name, last_name))`); err != nil {
		t.Fatal("create table:", err)
	}

	m := FullName{"John", "Doe"}

	if err := session.Query(`INSERT INTO struct_only_table (first_name, last_name) values (?, ?)`, nil).Bind(m.FirstName, m.LastName).Exec(); err != nil {
		t.Fatal("insert:", err)
	}

	const stmt = `SELECT first_name, last_name FROM struct_only_table`

	t.Run("get", func(t *testing.T) {
		var v FullName
		if err := session.Query(stmt, nil).Iter().StructOnly().Get(&v); err != nil {
			t.Fatal("Get() failed:", err)
		}
		if diff := cmp.Diff(m, v); diff != "" {
			t.Fatalf("Get()=%+v expected %+v, diff: %s", v, m, diff)
		}
	})

	t.Run("select", func(t *testing.T) {
		var v []FullName
		if err := session.Query(stmt, nil).Iter().StructOnly().Select(&v); err != nil {
			t.Fatal("Select() failed:", err)
		}
		if len(v) != 1 {
			t.Fatalf("Select()=%+v expected 1 row got %d", v, len(v))
		}
		if diff := cmp.Diff(m, v[0]); diff != "" {
			t.Fatalf("Select()[0]=%+v expected %+v, diff: %s", v[0], m, diff)
		}
	})

	t.Run("select ptr", func(t *testing.T) {
		var v []*FullName
		if err := session.Query(stmt, nil).Iter().StructOnly().Select(&v); err != nil {
			t.Fatal("Select() failed:", err)
		}
		if len(v) != 1 {
			t.Fatalf("Select()=%+v expected 1 row got %d", v, len(v))
		}
		if diff := cmp.Diff(&m, v[0]); diff != "" {
			t.Fatalf("Select()[0]=%+v expected %+v, diff: %s", v[0], &m, diff)
		}
	})

	const golden = "expected 1 column in result"

	t.Run("get error", func(t *testing.T) {
		var v FullName
		err := session.Query(stmt, nil).Get(&v)
		if err == nil || !strings.HasPrefix(err.Error(), golden) {
			t.Fatalf("Get() error=%q expected %s", err, golden)
		}
	})

	t.Run("select error", func(t *testing.T) {
		var v []FullName
		err := session.Query(stmt, nil).Select(&v)
		if err == nil || !strings.HasPrefix(err.Error(), golden) {
			t.Fatalf("Select() error=%q expected %s", err, golden)
		}
	})
}

func TestIterxStructOnlyUDT(t *testing.T) {
	session := gocqlxtest.CreateSession(t)
	defer session.Close()

	if err := session.ExecStmt(`CREATE TABLE gocqlx_test.struct_only_udt_table (first_name text, last_name text, PRIMARY KEY (first_name, last_name))`); err != nil {
		t.Fatal("create table:", err)
	}

	m := FullNameUDT{
		FullName: FullName{
			FirstName: "John",
			LastName:  "Doe",
		},
	}

	if err := session.Query(`INSERT INTO struct_only_udt_table (first_name, last_name) values (?, ?)`, nil).Bind(m.FirstName, m.LastName).Exec(); err != nil {
		t.Fatal("insert:", err)
	}

	const stmt = `SELECT first_name, last_name FROM struct_only_udt_table`

	t.Run("get", func(t *testing.T) {
		var v FullNameUDT
		if err := session.Query(stmt, nil).Iter().StructOnly().Get(&v); err != nil {
			t.Fatal("Get() failed:", err)
		}
		if diff := cmp.Diff(m, v); diff != "" {
			t.Fatalf("Get()=%+v expected %+v, diff: %s", v, m, diff)
		}
	})

	t.Run("select", func(t *testing.T) {
		var v []FullNameUDT
		if err := session.Query(stmt, nil).Iter().StructOnly().Select(&v); err != nil {
			t.Fatal("Select() failed:", err)
		}
		if len(v) != 1 {
			t.Fatalf("Select()=%+v expected 1 row got %d", v, len(v))
		}
		if diff := cmp.Diff(m, v[0]); diff != "" {
			t.Fatalf("Select()[0]=%+v expected %+v, diff: %s", v[0], m, diff)
		}
	})

	t.Run("select ptr", func(t *testing.T) {
		var v []*FullNameUDT
		if err := session.Query(stmt, nil).Iter().StructOnly().Select(&v); err != nil {
			t.Fatal("Select() failed:", err)
		}
		if len(v) != 1 {
			t.Fatalf("Select()=%+v expected 1 row got %d", v, len(v))
		}
		if diff := cmp.Diff(&m, v[0]); diff != "" {
			t.Fatalf("Select()[0]=%+v expected %+v, diff: %s", v[0], &m, diff)
		}
	})

	const golden = "expected 1 column in result"

	t.Run("get error", func(t *testing.T) {
		var v FullNameUDT
		err := session.Query(stmt, nil).Get(&v)
		if err == nil || !strings.HasPrefix(err.Error(), golden) {
			t.Fatalf("Get() error=%q expected %s", err, golden)
		}
	})

	t.Run("select error", func(t *testing.T) {
		var v []FullNameUDT
		err := session.Query(stmt, nil).Select(&v)
		if err == nil || !strings.HasPrefix(err.Error(), golden) {
			t.Fatalf("Select() error=%q expected %s", err, golden)
		}
	})
}

func TestIterxStrict(t *testing.T) {
	session := gocqlxtest.CreateSession(t)
	defer session.Close()

	if err := session.ExecStmt(`CREATE TABLE gocqlx_test.strict_table (testtext text PRIMARY KEY, testtextunbound text)`); err != nil {
		t.Fatal("create table:", err)
	}
	if err := session.Query(`INSERT INTO strict_table (testtext, testtextunbound) values (?, ?)`, nil).Bind("test", "test").Exec(); err != nil {
		t.Fatal("insert:", err)
	}

	type StrictTable struct {
		Testtext string
	}

	m := StrictTable{
		Testtext: "test",
	}

	const (
		stmt   = `SELECT * FROM strict_table`
		golden = "missing destination name \"testtextunbound\" in gocqlx_test.StrictTable"
	)

	t.Run("get strict", func(t *testing.T) {
		var v StrictTable
		err := session.Query(stmt, nil).Strict().Get(&v)
		if err == nil || !strings.HasPrefix(err.Error(), golden) {
			t.Fatalf("Get() error=%q expected %s", err, golden)
		}
	})

	t.Run("select strict", func(t *testing.T) {
		var v []StrictTable
		err := session.Query(stmt, nil).Strict().Select(&v)
		if err == nil || !strings.HasPrefix(err.Error(), golden) {
			t.Fatalf("Select() error=%q expected %s", err, golden)
		}
		if cap(v) > 0 {
			t.Fatalf("Select() effect alloc cap=%d expected 0", cap(v))
		}
	})

	t.Run("get", func(t *testing.T) {
		var v StrictTable
		err := session.Query(stmt, nil).Get(&v)
		if err != nil {
			t.Fatal("Get() failed:", err)
		}
		if diff := cmp.Diff(m, v); diff != "" {
			t.Fatalf("Get()=%+v expected %+v, diff: %s", v, m, diff)
		}
	})

	t.Run("select", func(t *testing.T) {
		var v []StrictTable
		err := session.Query(stmt, nil).Select(&v)
		if err != nil {
			t.Fatal("Select() failed:", err)
		}
		if len(v) != 1 {
			t.Fatalf("Select()=%+v expected 1 row got %d", v, len(v))
		}
		if diff := cmp.Diff(m, v[0]); diff != "" {
			t.Fatalf("Select()[0]=%+v expected %+v, diff: %s", v[0], m, diff)
		}
	})

	t.Run("select default", func(t *testing.T) {
		var v []StrictTable
		err := session.Query(stmt, nil).Iter().Select(&v)
		if err != nil {
			t.Fatal("Select() failed:", err)
		}
		if len(v) != 1 {
			t.Fatalf("Select()=%+v expected 1 row got %d", v, len(v))
		}
		if diff := cmp.Diff(m, v[0]); diff != "" {
			t.Fatalf("Select()[0]=%+v expected %+v, diff: %s", v[0], m, diff)
		}
	})
}

func TestIterxNotFound(t *testing.T) {
	session := gocqlxtest.CreateSession(t)
	defer session.Close()

	if err := session.ExecStmt(`CREATE TABLE gocqlx_test.not_found_table (testtext text PRIMARY KEY)`); err != nil {
		t.Fatal("create table:", err)
	}

	type NotFoundTable struct {
		Testtext string
	}

	t.Run("get cql error", func(t *testing.T) {
		var v NotFoundTable
		err := session.Query(`SELECT * FROM not_found_table WRONG`, nil).RetryPolicy(nil).Get(&v)
		if err == nil || !strings.Contains(err.Error(), "WRONG") {
			t.Fatalf("Get() error=%q", err)
		}
	})

	t.Run("get", func(t *testing.T) {
		var v NotFoundTable
		err := session.Query(`SELECT * FROM not_found_table`, nil).Get(&v)
		if err != gocql.ErrNotFound {
			t.Fatalf("Get() error=%q expected %s", err, gocql.ErrNotFound)
		}
	})

	t.Run("select cql error", func(t *testing.T) {
		var v []NotFoundTable
		err := session.Query(`SELECT * FROM not_found_table WRONG`, nil).RetryPolicy(nil).Select(&v)
		if err == nil || !strings.Contains(err.Error(), "WRONG") {
			t.Fatalf("Get() error=%q", err)
		}
	})

	t.Run("select", func(t *testing.T) {
		var v []NotFoundTable
		err := session.Query(`SELECT * FROM not_found_table`, nil).Select(&v)
		if err != nil {
			t.Fatalf("Select() error=%q expected %s", err, gocql.ErrNotFound)
		}
		if cap(v) > 0 {
			t.Fatalf("Select() effect alloc cap=%d expected 0", cap(v))
		}
	})
}

func TestIterxErrorOnNil(t *testing.T) {
	session := gocqlxtest.CreateSession(t)
	defer session.Close()

	if err := session.ExecStmt(`CREATE TABLE gocqlx_test.nil_table (testtext text PRIMARY KEY)`); err != nil {
		t.Fatal("create table:", err)
	}

	const (
		stmt   = "SELECT * FROM not_found_table WRONG"
		golden = "expected a pointer but got <nil>"
	)

	t.Run("get", func(t *testing.T) {
		err := session.Query(stmt, nil).Get(nil)
		if err == nil || err.Error() != golden {
			t.Fatalf("Get() error=%q expected %q", err, golden)
		}
	})
	t.Run("select", func(t *testing.T) {
		err := session.Query(stmt, nil).Select(nil)
		if err == nil || err.Error() != golden {
			t.Fatalf("Select() error=%q expected %q", err, golden)
		}
	})
	t.Run("struct scan", func(t *testing.T) {
		iter := session.Query(stmt, nil).Iter()
		iter.StructScan(nil)
		err := iter.Close()
		if err == nil || err.Error() != golden {
			t.Fatalf("StructScan() error=%q expected %q", err, golden)
		}
	})
}

func TestIterxPaging(t *testing.T) {
	session := gocqlxtest.CreateSession(t)
	defer session.Close()

	if err := session.ExecStmt(`CREATE TABLE gocqlx_test.paging_table (id int PRIMARY KEY, val int)`); err != nil {
		t.Fatal("create table:", err)
	}
	if err := session.ExecStmt(`CREATE INDEX id_val_index ON gocqlx_test.paging_table (val)`); err != nil {
		t.Fatal("create index:", err)
	}

	q := session.Query(qb.Insert("gocqlx_test.paging_table").Columns("id", "val").ToCql())
	for i := 0; i < 5000; i++ {
		if err := q.Bind(i, i).Exec(); err != nil {
			t.Fatal(err)
		}
	}

	type Paging struct {
		ID  int
		Val int
	}

	stmt, names := qb.Select("gocqlx_test.paging_table").
		Where(qb.Lt("val")).
		AllowFiltering().
		Columns("id", "val").ToCql()
	iter := session.Query(stmt, names).Bind(100).PageSize(10).Iter()
	defer func() {
		_ = iter.Close()
	}()

	var cnt int
	for {
		p := &Paging{}
		if !iter.StructScan(p) {
			break
		}
		cnt++
	}
	if cnt != 100 {
		t.Fatal("expected 100", "got", cnt)
	}
}

func TestIterxCAS(t *testing.T) {
	session := gocqlxtest.CreateSession(t)
	defer session.Close()

	const (
		id         = 0
		baseSalary = 1000
		minSalary  = 2000
	)

	john := struct {
		ID     int
		Salary int
	}{ID: id, Salary: baseSalary}

	if err := session.ExecStmt(`CREATE TABLE gocqlx_test.cas_table (id int PRIMARY KEY, salary int)`); err != nil {
		t.Fatal("create table:", err)
	}

	insert := session.Query(qb.Insert("cas_table").Columns("id", "salary").Unique().ToCql())

	applied, err := insert.BindStruct(john).ExecCAS()
	if err != nil {
		t.Fatal("ExecCAS() failed:", err)
	}
	if !applied {
		t.Error("ExecCAS() expected first insert success")
	}

	applied, err = insert.BindStruct(john).ExecCAS()
	if err != nil {
		t.Fatal("ExecCAS() failed:", err)
	}
	if applied {
		t.Error("ExecCAS() Expected second insert to not be applied")
	}

	update := session.Query(qb.Update("cas_table").
		SetNamed("salary", "min_salary").
		Where(qb.Eq("id")).
		If(qb.LtNamed("salary", "min_salary")).
		ToCql(),
	)

	applied, err = update.BindStructMap(john, qb.M{"min_salary": minSalary}).GetCAS(&john)
	if err != nil {
		t.Fatal("GetCAS() failed:", err)
	}
	if !applied {
		t.Error("GetCAS() expected update to be applied")
	}
	if john.Salary != baseSalary {
		t.Error("GetCAS()=%=v expected to have pre-image", john)
	}

	applied, err = update.BindStructMap(john, qb.M{"min_salary": minSalary * 2}).GetCAS(&john)
	if err != nil {
		t.Fatal(err)
	}
	if !applied {
		t.Error("GetCAS() expected update to be applied")
	}
	if john.Salary != minSalary {
		t.Error("GetCAS()=%=v expected to have pre-image", john)
	}
}
